﻿// 2025/10/16: 首个版本
// 2025/11/12: 优化tab control显示

using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Xml;
using System.Text;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Avalonia.Media;
using Avalonia.Controls;
using ASEva;
using ASEva.UIAvalonia;
using ASEva.Utility;

namespace CommonSource
{
    partial class ConfigEditor : DialogPanel
    {
        public ConfigEditor() // For designer
        {
            InitializeComponent();
            language = new LanguageSwitch(Resources, AgencyLocal.GetAppLanguage());
        }

        public ConfigEditor(object caller, String iconResourceName = null, RelatedGUISettings guiSettings = null)
        {
            InitializeComponent();
            SetResizableMode(880, 400, 1100, 600);
            language = new LanguageSwitch(Resources, AgencyLocal.GetAppLanguage());
            Icon = ResourceLoader.Load(iconResourceName)?.ToAvaloniaWindowIcon();
            Title = language["title"];

            Model = new ConfigEditorViewModel(this, caller, language, guiSettings ?? new RelatedGUISettings(), iconResourceName);
            Model.InitializeList();
            DataContext = Model;
        }

        public ConfigEditorViewModel Model { get; private set; }

        private LanguageSwitch language;
    }

    partial class ConfigEditorViewModel(Control view, object caller, LanguageSwitch language, RelatedGUISettings guiSettings, String iconResourceName) : ObservableObject
    {
        // Module list ////////////////////////////////////////////////////////////////////////////////////

        public async void InitializeList()
        {
            var fullList = new List<ModuleItem>();
            foreach (var pair in await AgencyAsync.GetProcessorClassTable())
            {
                fullList.Add(new ModuleItem(pair.Key, pair.Value.Title));
            }
            foreach (var pair in await AgencyAsync.GetNativeClassTable())
            {
                fullList.Add(new ModuleItem(pair.Key, pair.Value.Title));
            }
            foreach (var pair in await AgencyAsync.GetDeviceClassTable())
            {
                fullList.Add(new ModuleItem(pair.Key, pair.Value.Title));
            }
            allItems = fullList.ToArray();

            foreach (var item in allItems.OrderBy(item => item.Title))
            {
                ModuleItems.Add(item);
            }
        }

        public partial class ModuleItem(String id, String name) : ObservableObject
        {
            public string ID => id;

            public string Title => name;
        }

        private ModuleItem[] allItems = [];

        [ObservableProperty]
        private ObservableCollection<ModuleItem> moduleItems = new ObservableCollection<ModuleItem>();

        [ObservableProperty]
        private ModuleItem selectedModuleItem = null;

        partial void OnSelectedModuleItemChanged(ModuleItem value)
        {
            onSelectedModuleItemChanged(value?.ID);
        }

        private async void onSearchOrFilterChanged()
        {
            var key = SearchText == null ? "" : SearchText.ToLower();
            var filterList = new List<ModuleItem>();
            foreach (var item in allItems)
            {
                if (key.Length == 0) filterList.Add(item);
                else
                {
                    if (item.ID.ToLower().Contains(key) || item.Title.ToLower().Contains(key)) filterList.Add(item);
                }
            }
            filterList.Sort((a, b) => a.Title.CompareTo(b.Title));
            var midItems = filterList.ToArray();

            var tasks = new Task<(ConfigStatus, String)>[midItems.Length].Populate((i) =>
            {
                return AgencyAsync.GetModuleConfigStatus(caller, midItems[i].ID);
            });
            var results = await Task.WhenAll(tasks);

            var curKey = SearchText == null ? "" : SearchText.ToLower();
            if (curKey != key) return;

            ModuleItems.Clear();
            for (int i = 0; i < midItems.Length; i++)
            {
                var item = midItems[i];
                var result = results[i];
                switch (result.Item1)
                {
                    case ConfigStatus.Disabled:
                        if (!FilterDisabled) continue;
                        break;
                    case ConfigStatus.Enabled:
                        if (!FilterOK) continue;
                        break;
                    case ConfigStatus.EnabledWithError:
                        if (!FilterError) continue;
                        break;
                    case ConfigStatus.EnabledWithWarning:
                        if (!FilterWarning) continue;
                        break;
                }
                ModuleItems.Add(item);
            }
        }

        [ObservableProperty]
        private string searchText = "";

        partial void OnSearchTextChanged(string value)
        {
            onSearchOrFilterChanged();
        }

        [ObservableProperty]
        private bool filterOK = true;

        partial void OnFilterOKChanged(bool value)
        {
            onSearchOrFilterChanged();
        }

        [ObservableProperty]
        private bool filterDisabled = true;

        partial void OnFilterDisabledChanged(bool value)
        {
            onSearchOrFilterChanged();
        }

        [ObservableProperty]
        private bool filterError = true;

        partial void OnFilterErrorChanged(bool value)
        {
            onSearchOrFilterChanged();
        }

        [ObservableProperty]
        private bool filterWarning = true;

        partial void OnFilterWarningChanged(bool value)
        {
            onSearchOrFilterChanged();
        }

        // Module info ////////////////////////////////////////////////////////////////////////////////////

        public partial class ModuleInfo : ObservableObject
        {
            [ObservableProperty]
            private string mainStatus = "";

            [ObservableProperty]
            private string childrenStatus = "";

            [ObservableProperty]
            private IBrush mainStatusForeground = Brushes.Black;

            [ObservableProperty]
            private string outputSamples = "";

            [ObservableProperty]
            private string outputSignals = "";

            [ObservableProperty]
            private string outputScenes = "";

            [ObservableProperty]
            private string outputReports = "";

            [ObservableProperty]
            private string requiredSignalPackings = "";

            [ObservableProperty]
            private string requiredVideoChannels = "";

            [ObservableProperty]
            private string requiredDataTypes = "";
        }

        [ObservableProperty]
        private ModuleInfo selectedModuleInfo = null;

        [ObservableProperty]
        private string configText = "";

        [ObservableProperty]
        [NotifyPropertyChangedFor(nameof(CanEnable))]
        private bool enableModuleAvailable = false;

        [ObservableProperty]
        [NotifyPropertyChangedFor(nameof(CanDisable))]
        private bool disableModuleAvailable = false;

        [ObservableProperty]
        [NotifyPropertyChangedFor(nameof(CanEditAsXML))]
        private bool editAsXMLAvailable = false;

        [ObservableProperty]
        [NotifyPropertyChangedFor(nameof(CanEditWithGUI))]
        private ConfigEditorGUIChooserViewModel guiChooserModel = null;

        [ObservableProperty]
        [NotifyPropertyChangedFor(nameof(CanEnable))]
        [NotifyPropertyChangedFor(nameof(CanDisable))]
        [NotifyPropertyChangedFor(nameof(CanEditAsXML))]
        [NotifyPropertyChangedFor(nameof(CanEditWithGUI))]
        private bool editingConfigText = false;

        public bool CanEnable => EnableModuleAvailable && !EditingConfigText;

        public bool CanDisable => DisableModuleAvailable && !EditingConfigText;

        public bool CanEditAsXML => EditAsXMLAvailable && !EditingConfigText;

        public bool CanEditWithGUI => GuiChooserModel != null && !EditingConfigText;

        private async void onSelectedModuleItemChanged(String id)
        {
            SelectedModuleInfo = null;
            EditingConfigText = false;
            if (id == null) return;

            Task[] tasks =
            [
                AgencyAsync.GetModuleConfig(caller, id),
                AgencyAsync.GetModuleConfigStatus(caller, id),
                AgencyAsync.GetModuleChildConfigStatus(caller, id),
                AgencyAsync.IsEnableModuleSupported(id),
                AgencyAsync.IsDisableModuleSupported(id),
                AgencyAsync.GetModuleDetails(id),
            ];
            await Task.WhenAll(tasks);

            if (SelectedModuleItem == null || SelectedModuleItem.ID != id) return;

            var moduleConfigText = (tasks[0] as Task<String>).Result;
            var moduleConfigStatus = (tasks[1] as Task<(ConfigStatus, String)>).Result;
            var moduleChildConfigStatus = (tasks[2] as Task<ConfigStatus[]>).Result;
            var moduleEnableSupported = (tasks[3] as Task<bool>).Result;
            var moduleDisableSupported = (tasks[4] as Task<bool>).Result;
            var moduleDetails = (tasks[5] as Task<ModuleDetails>).Result;

            SelectedModuleInfo = new ModuleInfo();

            var brush = App.DarkTheme ? Brushes.LightGray : Brushes.Black;
            String text = "";
            switch (moduleConfigStatus.Item1)
            {
                case ConfigStatus.Disabled:
                    brush = Brushes.Gray;
                    text = language["disabled"];
                    break;
                case ConfigStatus.Enabled:
                    brush = App.DarkTheme ? Brushes.PaleGreen : Brushes.Green;
                    text = language["ok"];
                    break;
                case ConfigStatus.EnabledWithError:
                    brush = App.DarkTheme ? Brushes.Tomato : Brushes.Red;
                    text = language["error"];
                    break;
                case ConfigStatus.EnabledWithWarning:
                    brush = Brushes.DarkOrange;
                    text = language["warning"];
                    break;
                default:
                    text = language["unknown"];
                    break;
            }
            if (!String.IsNullOrWhiteSpace(moduleConfigStatus.Item2)) text = text + " (" + moduleConfigStatus.Item2 + ")";
            SelectedModuleInfo.MainStatus = text;
            SelectedModuleInfo.MainStatusForeground = brush;

            if (moduleChildConfigStatus != null)
            {
                var textList = new List<String>();
                foreach (var s in moduleChildConfigStatus)
                {
                    switch (s)
                    {
                        case ConfigStatus.Disabled:
                            textList.Add(language["disabled"]);
                            break;
                        case ConfigStatus.Enabled:
                            textList.Add(language["ok"]);
                            break;
                        case ConfigStatus.EnabledWithError:
                            textList.Add(language["error"]);
                            break;
                        case ConfigStatus.EnabledWithWarning:
                            textList.Add(language["warning"]);
                            break;
                        default:
                            textList.Add(language["unknown"]);
                            break;
                    }
                }
                SelectedModuleInfo.ChildrenStatus = String.Join(", ", textList);
            }
            else SelectedModuleInfo.ChildrenStatus = "---";

            if (moduleDetails != null)
            {
                var sampleRows = new List<String>();
                if (moduleDetails.OutputSamples != null) foreach (var pair in moduleDetails.OutputSamples) sampleRows.Add(pair.Value + " (" + pair.Key + ")");
                SelectedModuleInfo.OutputSamples = String.Join('\n', sampleRows);

                var signalRows = new List<String>();
                if (moduleDetails.OutputSignals != null)
                {
                    foreach (var pair in moduleDetails.OutputSignals)
                    {
                        if (pair.Value == null || pair.Value.Length == 0) continue;
                        var prefix = String.IsNullOrWhiteSpace(pair.Key) ? "" : (pair.Key + " -> ");
                        foreach (var name in pair.Value) signalRows.Add(prefix + name);
                    }
                }
                SelectedModuleInfo.OutputSignals = String.Join('\n', signalRows);

                SelectedModuleInfo.OutputScenes = moduleDetails.OutputScenes == null ? "" : String.Join('\n', moduleDetails.OutputScenes);
                SelectedModuleInfo.OutputReports = moduleDetails.OutputGraphs == null ? "" : String.Join('\n', moduleDetails.OutputGraphs);
                SelectedModuleInfo.RequiredSignalPackings = moduleDetails.RequiredSignalPackings == null ? "" : String.Join('\n', moduleDetails.RequiredSignalPackings);
                SelectedModuleInfo.RequiredVideoChannels = moduleDetails.RequiredVideoChannels == null ? "" : String.Join('\n', moduleDetails.RequiredVideoChannels);
                SelectedModuleInfo.RequiredDataTypes = moduleDetails.RequiredRecordDataTypes == null ? "" : String.Join('\n', moduleDetails.RequiredRecordDataTypes);
            }

            EnableModuleAvailable = moduleEnableSupported && moduleConfigStatus.Item1 == ConfigStatus.Disabled;
            DisableModuleAvailable = moduleDisableSupported && moduleConfigStatus.Item1 != ConfigStatus.Disabled;

            String niceXmlString;
            bool isXml = checkIsXml(moduleConfigText, out niceXmlString);
            EditAsXMLAvailable = isXml;
            ConfigText = isXml ? niceXmlString : moduleConfigText;

            var entries = new List<ConfigEditorGUIChooserViewModel.EntryItem>();
            if (moduleDetails != null && moduleDetails.RelatedUIEntries != null)
            {
                foreach (var e in moduleDetails.RelatedUIEntries)
                {
                    String typeText = null;
                    switch (e.Type)
                    {
                        case ModuleRelatedUIType.Dialog:
                            if (guiSettings.EnableDialogs) typeText = language["dialog"];
                            break;
                        case ModuleRelatedUIType.Window:
                            if (guiSettings.EnableWindows) typeText = language["window"];
                            break;
                        case ModuleRelatedUIType.Console:
                            if (guiSettings.EnableConsoles) typeText = language["console"];
                            break;
                    }
                    if (typeText != null)
                    {
                        entries.Add(new ConfigEditorGUIChooserViewModel.EntryItem(typeText + e.Name, e));
                    }
                }
            }
            if (entries.Count == 0) GuiChooserModel = null;
            else GuiChooserModel = new ConfigEditorGUIChooserViewModel(entries);
        }

        private bool checkIsXml(String str, out String niceXmlString)
        {
            niceXmlString = "";
            if (str == null) str = "";
            if (str.StartsWith("<?xml"))
            {
                var xml = new XmlDocument();
                try
                {
                    xml.LoadXml(str);
                    niceXmlString = toNiceString(xml);
                    return true;
                }
                catch (Exception) { }
            }
            return false;
        }

        private String toNiceString(XmlDocument xml)
        {
            if (xml == null) return "";

            var memoryStream = new MemoryStream();

            var settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.IndentChars = "    ";

            var xmlWriter = XmlWriter.Create(memoryStream, settings);
            xml.Save(xmlWriter);
            xmlWriter.Close();

            memoryStream.Position = 3; // BOM
            var data = new byte[memoryStream.Length - 3];
            memoryStream.Read(data, 0, (int)memoryStream.Length - 3);
            memoryStream.Close();

            return Encoding.UTF8.GetString(data);
        }

        [RelayCommand]
        private async Task Import()
        {
            var selected = await App.ShowOpenFileDialog(view, null, false, null, new()
            {
                [language["suffix-asmc"]] = [".asmc"],
            });
            if (selected == null) return;

            var moduleID = SelectedModuleItem?.ID;
            if (moduleID == null) return;

            var xml = new XmlDocument();
            try { xml.Load(selected[0]); }
            catch (Exception)
            {
                await AgencyLocal.PopupError(language["failed-to-load-config"]);
                return;
            }

            var rootNode = xml.DocumentElement;
            var typeName = rootNode.Attributes["type"].Value.ToLower();

            if (typeName != moduleID)
            {
                await AgencyLocal.PopupError(language["mismatch-config-type"]);
                return;

            }

            await AgencyAsync.SetModuleConfig(caller, moduleID, Encoding.UTF8.GetString(Convert.FromBase64String(rootNode.InnerText)));
            if (SelectedModuleItem == null || SelectedModuleItem.ID != moduleID) return;

            onSelectedModuleItemChanged(moduleID);
        }

        [RelayCommand]
        private async Task Export()
        {
            var selected = await App.ShowSaveFileDialog(view, null, Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), SelectedModuleItem?.Title, language["suffix-asmc"], ".asmc");
            if (selected == null) return;

            var moduleID = SelectedModuleItem?.ID;
            if (moduleID == null) return;

            var config = await AgencyAsync.GetModuleConfig(caller, moduleID);
            if (config == null) config = "";

            var xml = Xml.Create("root");
            var root = xml.DocumentElement;
            root.Attributes.Append(xml.CreateAttribute("type")).Value = moduleID;
            root.InnerText = Convert.ToBase64String(Encoding.UTF8.GetBytes(config));
            xml.Save(selected);

            AgencyLocal.Log(language.Format("config-saved", selected), LogLevel.Info);
        }

        [RelayCommand]
        private async Task Enable()
        {
            var moduleID = SelectedModuleItem?.ID;
            if (moduleID == null) return;

            await AgencyAsync.EnableModule(caller, moduleID);
            if (SelectedModuleItem == null || SelectedModuleItem.ID != moduleID) return;

            onSelectedModuleItemChanged(moduleID);
        }

        [RelayCommand]
        private async Task Disable()
        {
            var moduleID = SelectedModuleItem?.ID;
            if (moduleID == null) return;

            await AgencyAsync.DisableModule(caller, moduleID);
            if (SelectedModuleItem == null || SelectedModuleItem.ID != moduleID) return;

            onSelectedModuleItemChanged(moduleID);
        }

        [RelayCommand]
        private async Task EditWithGUI()
        {
            var moduleID = SelectedModuleItem?.ID;
            if (moduleID == null || GuiChooserModel == null) return;

            ModuleRelatedUIEntry entry = null;
            if (GuiChooserModel.EntryItems.Count > 0/* temp */)
            {
                if (GuiChooserModel.SelectedEntryItem == null) GuiChooserModel.SelectedEntryItem = GuiChooserModel.EntryItems[0];
                var chooser = new ConfigEditorGUIChooser(GuiChooserModel);
                await App.RunDialog(chooser);
                if (chooser.BoolResult) entry = GuiChooserModel.SelectedEntryItem.Entry;
                else return;
            }
            else entry = GuiChooserModel.EntryItems[0].Entry;

            bool handled = false;
            if (entry.Type == ModuleRelatedUIType.Dialog)
            {
                if (OpenDialog != null) handled = await OpenDialog.Invoke(entry.ClassID, entry.TransformID);
            }
            else if (entry.Type == ModuleRelatedUIType.Window)
            {
                if (AddWindow != null) handled = await AddWindow.Invoke(entry.ClassID, entry.TransformID);
            }
            else if (entry.Type == ModuleRelatedUIType.Console)
            {
                if (RunConsole != null) handled = await RunConsole.Invoke(entry.ClassID);
            }
            if (!handled)
            {
                await AgencyLocal.PopupError(language["failed-to-open-gui"]);
                return;
            }

            if (SelectedModuleItem == null || SelectedModuleItem.ID != moduleID) return;

            onSelectedModuleItemChanged(moduleID);
        }

        [RelayCommand]
        private async Task EditAsXML()
        {
            var moduleID = SelectedModuleItem?.ID;
            if (moduleID == null) return;

            var dialog = new XmlEditor(ConfigText, iconResourceName);
            await App.RunDialog(dialog);
            if (!dialog.BoolResult) return;

            await AgencyAsync.SetModuleConfig(caller, moduleID, dialog.XmlString);
            if (SelectedModuleItem == null || SelectedModuleItem.ID != moduleID) return;

            onSelectedModuleItemChanged(moduleID);
        }

        [RelayCommand]
        private void Edit()
        {
            EditingConfigText = true;
        }

        [RelayCommand]
        private async Task Save()
        {
            EditingConfigText = false;

            var moduleID = SelectedModuleItem?.ID;
            if (moduleID == null) return;

            await AgencyAsync.SetModuleConfig(caller, moduleID, ConfigText);
            if (SelectedModuleItem == null || SelectedModuleItem.ID != moduleID) return;

            onSelectedModuleItemChanged(moduleID);
        }

        // Events ////////////////////////////////////////////////////////////////////////////////////////

        public delegate Task<bool> AddWindowHandler(String classID, String transformID);
        public delegate Task<bool> OpenDialogHandler(String classID, String transformID);
        public delegate Task<bool> RunConsoleHandler(String classID);

        public event AddWindowHandler AddWindow;
        public event OpenDialogHandler OpenDialog;
        public event RunConsoleHandler RunConsole;
    }

    class RelatedGUISettings
    {
        public bool EnableDialogs { get; set; }
        public bool EnableWindows { get; set; }
        public bool EnableConsoles { get; set; }

        public RelatedGUISettings(bool enableDialogs = true, bool enableWindows = false, bool enableConsoles = false)
        {
            EnableDialogs = enableDialogs;
            EnableWindows = enableWindows;
            EnableConsoles = enableConsoles;
        }
    }

    partial class ConfigEditorGUIChooserViewModel(List<ConfigEditorGUIChooserViewModel.EntryItem> entries) : ObservableObject
    {
        public partial class EntryItem(String title, ModuleRelatedUIEntry entry) : ObservableObject
        {
            public string Title => title;
            public ModuleRelatedUIEntry Entry => entry;
        }

        [ObservableProperty]
        private ObservableCollection<EntryItem> entryItems = new ObservableCollection<EntryItem>(entries);

        [ObservableProperty]
        private EntryItem selectedEntryItem = null;

        [RelayCommand]
        private void Open()
        {
            OpenRequested?.Invoke(this, EventArgs.Empty);
        }
        
        public event EventHandler OpenRequested;
    }

    partial class ConfigEditorGUIChooser : DialogPanel
    {
        public ConfigEditorGUIChooser() // For designer
        {
            InitializeComponent();
            language = new LanguageSwitch(Resources, AgencyLocal.GetAppLanguage());
        }

        public ConfigEditorGUIChooser(ConfigEditorGUIChooserViewModel model, String iconResourceName = null)
        {
            InitializeComponent();
            SetFixMode(600, 60, true);
            language = new LanguageSwitch(Resources, AgencyLocal.GetAppLanguage());
            Icon = ResourceLoader.Load(iconResourceName)?.ToAvaloniaWindowIcon();
            Title = language["title"];
            DataContext = model;

            model.OpenRequested += delegate
            {
                BoolResult = true;
                Close();
            };
        }

        private LanguageSwitch language;
    }
}